﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Direct.EntityStore;
using Direct.Security.Model;
using Direct.Security.Impl;
using Direct.Core;

namespace Direct.Security
{
    class AuthorizationService : IAuthorizationService
    {
        private IEntityStore _store;
        private Dictionary<String, AuthorizationKeyImpl> _keys = new Dictionary<string, AuthorizationKeyImpl>();
        private Dictionary<String, AuthorizationStatus> _cache = new Dictionary<string, AuthorizationStatus>();
        private Dictionary<String, AuthorizationEffectiveRule> _effectiveRulesCache = new Dictionary<String, AuthorizationEffectiveRule>();

        public AuthorizationService()
        {
            _store = ServiceLocator.GetInstance<IEntityStoreManager>().CreateStore("auth");
            _store.RegisterEntity<AuthKeyStatEntity>();
            _keys["/"] = new AuthorizationKeyImpl(this, null, "/", "Raiz", 0);
            SetObjectStatus("system", "/", AuthorizationStatus.Grant);
        }

        public bool IsUserAuthorized(string username, string authorizationKey)
        {
            if (username.EqualsIgnoreCase("system"))
                return true;
            return GetEffectiveObjectStatus(username, authorizationKey).EffectiveStatus == AuthorizationStatus.Grant;
        }

        public IAuthorizationKey CreateKey(string keyPath, string caption, int order = 0)
        {
            Assert.EmptyString(keyPath, "keyPath");
            var path = keyPath.Split(new Char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            var pathStr = "";
            AuthorizationKeyImpl parent = GetKeyInternal("/", true);
            for (var i = 0; i < path.Length; i++)
            {
                var part = path[i];
                pathStr += "/" + part;
                var key = GetKeyInternal(pathStr);
                if (key == null)
                {
                    key = i < path.Length - 1 ?
                        new AuthorizationKeyImpl(this, parent, pathStr, part, 0) :
                        new AuthorizationKeyImpl(this, parent, pathStr, caption, order);
                    this._keys[NormalizePath(key.AuthorizationKey)] = key;
                    parent.AddItem(key);
                }
                parent = key;
            }
            return parent;
        }

        public IAuthorizationKey CreateKey(string parentKey, string key, string caption, int order = 0)
        {
            return CreateKey(String.Format("{0}/{1}", parentKey, key), caption, order);
        }

        private String NormalizePath(String path)
        {
            path = path.Replace("//", "/");
            if (path == "/")
                return path;
            if (path.EndsWith("/"))
                path = path.Substring(0, path.Length - 1);
            if (!path.StartsWith("/"))
                path = "/" + path;
            return path.ToLower();
        }

        private AuthorizationKeyImpl GetKeyInternal(string basePath, bool throwException = false)
        {
            if (basePath == "")
                basePath = "/";
            AuthorizationKeyImpl result = null;
            _keys.TryGetValue(NormalizePath(basePath), out result);
            if (result == null && throwException)
                throw new InvalidOperationException("Authorization key not found: " + basePath);
            return result;
        }

        public IAuthorizationKey GetKey(string basePath = "")
        {
            return GetKeyInternal(basePath, false);
        }

        private AuthKeyStatEntity GetModel(String objId, String authKey, bool createIfNotExists)
        {
            Assert.EmptyString(objId, "objId");
            Assert.EmptyString(authKey, "authorizationKey");
            var qry = _store.CreateQuery<AuthKeyStatEntity>();
            qry.AddFilterEqual("ObjId", objId);
            qry.AddFilterEqual("AuthKey", authKey);
            var result = qry.FindFirst();
            if (result == null && createIfNotExists)
            {
                result = new AuthKeyStatEntity()
                {
                    ObjId = objId,
                    AuthKey = authKey,
                    Status = AuthorizationStatus.Default
                };
            }
            return result;
        }

        private String CacheKey(string objId, string authKey)
        {
            return String.Format("{0}:{1}", objId, authKey);
        }

        public void SetObjectStatus(string objId, string authKey, AuthorizationStatus status)
        {
            var key = GetKeyInternal(authKey, true);
            var cacheKey = CacheKey( objId, authKey);
            var model = GetModel(objId, authKey, true);
            model.Status = status;
            if (_store.Save(model))
                this._cache[cacheKey] = status;
        }

        public AuthorizationStatus GetObjectStatus(string objId, string authKey)
        {
            var cacheKey = CacheKey(objId, authKey);
            AuthorizationStatus result = AuthorizationStatus.Default;
            if (_cache.TryGetValue(cacheKey, out result))
            {
                var model = GetModel(objId, authKey, false);
                if (model != null)
                    result = model.Status;
                _cache[cacheKey] = result;
            }
            return result;
        }

        private class AuthorizationEffectiveRule : IAuthorizationEffectiveRule
        {
            public IAuthorizationKey AuthorizationKey { get; internal set; }
            public ISecurityObject TargetObject { get; internal set; }
            public ISecurityObject EffectiveObject { get; internal set; }
            public AuthorizationStatus Status { get; internal set; }
            public AuthorizationStatus EffectiveStatus { get; internal set; }
        }

        public IAuthorizationEffectiveRule GetEffectiveObjectStatus(string objId, string authKey)
        {
            var accountService = ServiceLocator.GetInstance<IAccountService>();
            var secObj = accountService.GetObject(objId);
            var key = GetKeyInternal(authKey, true);

            return GetEffectiveObjectStatus(secObj, key, accountService);
        }

        private IAuthorizationEffectiveRule GetEffectiveObjectStatus(ISecurityObject securityObj, IAuthorizationKey authKey, IAccountService accountService)
        {
            var cacheKey = CacheKey(securityObj.ObjId, authKey.AuthorizationKey);
            var status = GetObjectStatus(securityObj.ObjId, authKey.AuthorizationKey);
            //procurar no cache primeiro
            AuthorizationEffectiveRule rule = null;
            if (!_effectiveRulesCache.TryGetValue(cacheKey, out rule))
            {
                rule = new AuthorizationEffectiveRule()
                {
                    TargetObject = securityObj,
                    Status = status,
                    AuthorizationKey = authKey
                };
                _effectiveRulesCache[cacheKey] = rule;

                if (status != AuthorizationStatus.Default)
                {
                    rule.EffectiveObject = securityObj;
                    rule.EffectiveStatus = status;
                }

                // se for negação, não continuar a verificação
                if (status == AuthorizationStatus.Deny)
                    return rule;

                Func<IAuthorizationEffectiveRule, bool> _checkRule = (sourceRule) =>
                {
                    if (sourceRule.EffectiveStatus != AuthorizationStatus.Default && sourceRule.EffectiveStatus != rule.EffectiveStatus)
                    {
                        rule.EffectiveObject = sourceRule.EffectiveObject;
                        rule.EffectiveStatus = sourceRule.EffectiveStatus;
                    }
                    return sourceRule.EffectiveStatus != AuthorizationStatus.Deny;
                };

                switch (securityObj.ObjType)
                {
                    case SecObjType.User:
                        //procurar nos roles
                        var user = securityObj as IUser;
                        foreach (var role in accountService.GetRolesForUser(securityObj.ObjId))
                        {
                            if (!_checkRule(GetEffectiveObjectStatus(role, authKey, accountService)))
                                return rule;
                        }


                        // verificar o esquema
                        if (!_checkRule(GetEffectiveObjectStatus(user.Schema, authKey, accountService)))
                            return rule;
                        break;

                    case SecObjType.Role:
                        // consultar apenas o role
                        var roleAuth = GetObjectStatus(securityObj.ObjId, authKey.AuthorizationKey);
                        if (roleAuth != AuthorizationStatus.Default && roleAuth != rule.EffectiveStatus)
                        {
                            rule.EffectiveStatus = roleAuth;
                            rule.EffectiveObject = securityObj;
                        }
                        break;

                    case SecObjType.Schema:
                        var schemaAuth = GetObjectStatus(securityObj.ObjId, authKey.AuthorizationKey);
                        var schema = securityObj as ISchema;
                        if (schemaAuth != AuthorizationStatus.Default && schemaAuth != rule.EffectiveStatus)
                        {
                            rule.EffectiveStatus = schemaAuth;
                            rule.EffectiveObject = securityObj;
                        }
                        break;
                }

                // pesquisar no default
                _checkRule(GetEffectiveObjectStatus(accountService.GetObject(SecObjType.Default, ""), authKey, accountService));
                _effectiveRulesCache[cacheKey] = rule;
            }
            return rule;
        }
    }
}
